/*
 * Copyright 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.ipc.invalidation.ticl.android;

import com.google.common.base.Preconditions;
import com.google.ipc.invalidation.external.client.InvalidationClient;
import com.google.ipc.invalidation.external.client.InvalidationListener;
import com.google.ipc.invalidation.external.client.SystemResources;
import com.google.ipc.invalidation.external.client.SystemResources.Logger;
import com.google.ipc.invalidation.external.client.android.AndroidInvalidationClient;
import com.google.ipc.invalidation.external.client.android.service.AndroidLogger;
import com.google.ipc.invalidation.external.client.android.service.Event;
import com.google.ipc.invalidation.external.client.android.service.ListenerBinder;
import com.google.ipc.invalidation.external.client.android.service.ListenerService;
import com.google.ipc.invalidation.external.client.android.service.ServiceBinder.BoundWork;
import com.google.ipc.invalidation.external.client.types.AckHandle;
import com.google.ipc.invalidation.external.client.types.ErrorInfo;
import com.google.ipc.invalidation.external.client.types.Invalidation;
import com.google.ipc.invalidation.external.client.types.ObjectId;
import com.google.ipc.invalidation.ticl.InvalidationClientCore;
import com.google.ipc.invalidation.ticl.InvalidationClientImpl;
import com.google.protos.ipc.invalidation.AndroidState.ClientMetadata;
import com.google.protos.ipc.invalidation.ClientProtocol.ClientConfigP;

import android.accounts.Account;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.http.AndroidHttpClient;

import java.util.Collection;
import java.util.Random;


/**
 * A bidirectional client proxy that wraps and delegates requests to a TICL instance and routes
 * events generated by the TICL back to the associated listener.
 *
 */
class AndroidClientProxy implements AndroidInvalidationClient {

  private static final Logger logger = AndroidLogger.forTag("InvClientProxy");

  /**
   * A reverse proxy for delegating raised invalidation events back to the client (via the
   * associated service).
   */
  class AndroidListenerProxy implements InvalidationListener {

    /** Binder that can be use to bind back to event listener service */
    
    final ListenerBinder binder;

    /**
     * Creates a new listener reverse proxy.
     */
    private AndroidListenerProxy() {
      this.binder = new ListenerBinder(service, Event.LISTENER_INTENT, metadata.getListenerClass());
    }

    @Override
    public void ready(InvalidationClient client) {
      Event event = Event.newBuilder(Event.Action.READY).setClientKey(clientKey).build();
      sendEvent(event);
    }

    @Override
    public void informRegistrationStatus(
        InvalidationClient client, ObjectId objectId, RegistrationState regState) {
      Event event = Event.newBuilder(Event.Action.INFORM_REGISTRATION_STATUS)
          .setClientKey(clientKey).setObjectId(objectId).setRegistrationState(regState).build();
      sendEvent(event);
    }

    @Override
    public void informRegistrationFailure(
        InvalidationClient client, ObjectId objectId, boolean isTransient, String errorMessage) {
      Event event = Event.newBuilder(Event.Action.INFORM_REGISTRATION_FAILURE)
          .setClientKey(clientKey).setObjectId(objectId).setIsTransient(isTransient)
          .setError(errorMessage).build();
      sendEvent(event);
    }

    @Override
    public void invalidate(
        InvalidationClient client, Invalidation invalidation, AckHandle ackHandle) {
      Event event = Event.newBuilder(Event.Action.INVALIDATE)
          .setClientKey(clientKey).setInvalidation(invalidation).setAckHandle(ackHandle).build();
      sendEvent(event);
    }

    @Override
    public void invalidateAll(InvalidationClient client, AckHandle ackHandle) {
      Event event = Event.newBuilder(Event.Action.INVALIDATE_ALL)
          .setClientKey(clientKey).setAckHandle(ackHandle).build();
      sendEvent(event);
    }

    @Override
    public void invalidateUnknownVersion(
        InvalidationClient client, ObjectId objectId, AckHandle ackHandle) {
      Event event = Event.newBuilder(Event.Action.INVALIDATE_UNKNOWN)
          .setClientKey(clientKey).setObjectId(objectId).setAckHandle(ackHandle).build();
      sendEvent(event);
    }

    @Override
    public void reissueRegistrations(InvalidationClient client, byte[] prefix, int prefixLength) {
      Event event = Event.newBuilder(Event.Action.REISSUE_REGISTRATIONS)
          .setClientKey(clientKey).setPrefix(prefix, prefixLength).build();
      sendEvent(event);
    }

    @Override
    public void informError(InvalidationClient client, ErrorInfo errorInfo) {
      Event event = Event.newBuilder(Event.Action.INFORM_ERROR)
          .setClientKey(clientKey).setErrorInfo(errorInfo).build();
      sendEvent(event);
    }

    /**
     * Releases any resources associated with the proxy listener.
     */
    public void release() {
      binder.release();
    }

    /**
     * Send event messages to application clients and provides common processing of the response.
     */
    private void sendEvent(final Event event) {
      binder.runWhenBound(new BoundWork<ListenerService>() {
        @Override
        public void run(ListenerService listenerService) {
          logger.fine("Sending %s event to %s", event.getAction(), clientKey);
          service.sendEvent(listenerService, event);
        }
      });
    }
  }

  /** The service associated with this proxy */
  private final AndroidInvalidationService service;

  /** the client key for this client proxy */
  private final String clientKey;

  /** The invalidation client to delegate requests to */
  
  final InvalidationClient delegate;

  /** The reverse listener proxy for this client proxy */
  private final AndroidListenerProxy listener;

  /** The stored state associated with this client */
  private final ClientMetadata metadata;

  /** The channel for this client */
  private final AndroidChannel channel;

  /** The system resources for this client */
  private final SystemResources resources;

  /** The HTTP client used by the underlying channel */
  private final AndroidHttpClient httpClient;

  /** {@code true} if client is started */
  private boolean started;

  /**
   * Creates a new client proxy instance.
   *
   * @param service the service within which the client proxy is executing.
   * @param storage the storage instance that contains client metadata and can be used to read or
   *        write client properties.
   */
  AndroidClientProxy(AndroidInvalidationService service, AndroidStorage storage,
      ClientConfigP config) {
    this.service = service;
    this.metadata = storage.getClientMetadata();
    this.clientKey = metadata.getClientKey();
    this.listener = new AndroidListenerProxy();
    this.httpClient =  AndroidChannel.getDefaultHttpClient(service);

    this.channel = new AndroidChannel(this, httpClient, service);
    this.resources =
        AndroidResourcesFactory.createResourcesBuilder(clientKey, channel, storage).build();
    String applicationName = getApplicationNameWithVersion(service,
        storage.getClientMetadata().getListenerPkg());
    this.delegate = createClient(resources, metadata.getClientType(), clientKey.getBytes(),
        applicationName, listener, config);
  }

  /**
   * Returns the application name string to pass to the Ticl, computed as a combination of the
   * listener package and the application version.
   */
  
  static String getApplicationNameWithVersion(Context context, String listenerPackage) {
    String appVersion = "unknown";
    try {
      PackageInfo packageInfo =
          context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
      String retrievedVersion = packageInfo.versionName;
      if (retrievedVersion != null) {
        appVersion = retrievedVersion;
      }
    } catch (NameNotFoundException exception) {
      // AndroidLogger does not use setSystemResources, so it's safe to use the logger here.
      logger.warning("Cannot retrieve current application version: %s", exception);
    }
    return listenerPackage + "#" + appVersion;
  }

  public final Account getAccount() {
    return new Account(metadata.getAccountName(), metadata.getAccountType());
  }

  public final String getAuthType() {
    return metadata.getAuthType();
  }

  @Override
  public final String getClientKey() {
    return metadata.getClientKey();
  }

  /** Returns the android service that is asociated with this proxy. */
  final AndroidInvalidationService getService() {
    return service;
  }

  /** Returns the network channel for this proxy. */
  final AndroidChannel getChannel() {
    return channel;
  }

  /** Returns the underlying invalidation client instance or {@code null} */
  
  final InvalidationClient getDelegate() {
    return delegate;
  }

  /** Returns the invalidation listener for this proxy */
  
  final AndroidListenerProxy getListener() {
    return listener;
  }

  /** Returns the storage used by the proxy. */
 final AndroidStorage getStorage() {
   return (AndroidStorage) resources.getStorage();
 }

  boolean isStarted() {
    return started;
  }

  @Override
  public void start() {
    if (started) {
      logger.info("Not starting Ticl since already started");
      return;
    }
    resources.start();
    delegate.start();
    started = true;
  }

  @Override
  public void stop() {
    // When a client is stopped, stop the TICL and its resources and remove it from the client
    // manager.   This means that any subsequent requests (like another start) will be executed
    // against a clean TICL instance w/ no preexisting state from before the stop.
    if (!started) {
      logger.info("Not stopping Ticl since already stopped");
      return;
    }
    stopTicl();
    resources.stop();
    AndroidInvalidationService.getClientManager().remove(clientKey);
  }

  @Override
  public void register(Collection<ObjectId> objectIds) {
    delegate.register(objectIds);
  }

  @Override
  public void register(ObjectId objectId) {
    delegate.register(objectId);
  }

  @Override
  public void unregister(Collection<ObjectId> objectIds) {
    delegate.unregister(objectIds);
  }

  @Override
  public void unregister(ObjectId objectId) {
    delegate.unregister(objectId);
  }

  @Override
  public void acknowledge(AckHandle ackHandle) {
    delegate.acknowledge(ackHandle);
  }

  /**
   * Called when the client proxy is being removed from memory and will no longer be in use.
   * Releases any resources associated with the client proxy.
   */
  @Override
  public void release() {
    // Release the listener associated with the proxy
    listener.release();

    // Stop system resources associated with the client
    if (resources.isStarted()) {
      resources.stop();
    }

    // Close the HTTP client
    httpClient.close();
  }

  @Override
  public void destroy() {

    // Stop the client if started.  This will also remove the client from the client manager
    if (started) {
      stop();
    }

    // Delete the storage associated with the client
    AndroidStorage storage = (AndroidStorage) resources.getStorage();
    storage.delete();

    // Remove any cached instance for this client.
    AndroidInvalidationService.getClientManager().remove(clientKey);
  }

  /**
   * Creates a new InvalidationClient instance that the proxy will delegate requests to and listen
   * for events from.
   */
  // Overridden by tests to inject mock clients or for listener interception
  
  InvalidationClient createClient(SystemResources resources, int clientType, byte[] clientName,
      String applicationName, InvalidationListener listener, ClientConfigP config) {
    // We always use C2DM, so set the channel-supports-offline-delivery bit on our config.
    final ClientConfigP.Builder configBuilder;
    if (config == null) {
      configBuilder = InvalidationClientCore.createConfig();
    } else {
      configBuilder = ClientConfigP.newBuilder(config);
    }
    configBuilder.setChannelSupportsOfflineDelivery(true);
    config = configBuilder.build();
    Random random = new Random(resources.getInternalScheduler().getCurrentTimeMs());
    return new InvalidationClientImpl(resources, random, clientType, clientName, config,
        applicationName, listener);
  }


  /** Stops the underlying TICL instance but does not stop system resources. */
  
  void stopTicl() {
    Preconditions.checkState(started);
    delegate.stop();
    started = false;
  }
}
